using Microsoft.AspNetCore.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Cryptography;

namespace AMS;

public static class App
{
    private const int PbKDF2IterCount = 1000;
    private const int PbKDF2SubkeyLength = 256 / 8;
    private const int SaltSize = 128 / 8;

    private static Func<object> _asyncLocalAccessor = null!;
    private static Func<object, object> _holderAccessor = null!;
    private static Func<object, HttpContext> _httpContextAccessor = null!;

    public static IServiceProvider Services { get; set; } = null!;
    public static Assembly Assembly { get; set; } = null!;

    public const char Separator = ' ';

    public static bool ManagedByDocker { get; set; } = false;

    public static string[] AdminRoleCodes { get; set; } = Array.Empty<string>();

    public static IDictionary<string, string> Metadata { get; } = new Dictionary<string, string>();
    public static HttpContext? HttpContext => GetHttpContext();
    public static ClaimsPrincipal? User => HttpContext?.User;

    private static HttpContext? GetHttpContext()
    {
        var asyncLocal = (_asyncLocalAccessor ??= CreateAsyncLocalAccessor())();

        if (asyncLocal is null)
        {
            return null;
        }

        var holder = (_holderAccessor ??= CreateHolderAccessor(asyncLocal))(asyncLocal);
        return holder is null ? null : (_httpContextAccessor ??= CreateHttpContextAccessor(holder))(holder);

        static Func<object> CreateAsyncLocalAccessor()
        {
            var fieldInfo = typeof(HttpContextAccessor).GetField("_httpContextCurrent", BindingFlags.Static | BindingFlags.NonPublic)!;
            var field = Expression.Field(null, fieldInfo);

            return Expression.Lambda<Func<object>>(field).Compile();
        }

        static Func<object, object> CreateHolderAccessor(object asyncLocal)
        {
            var holderType = asyncLocal.GetType().GetGenericArguments()[0];

            var method = typeof(AsyncLocal<>).MakeGenericType(holderType).GetProperty("Value")!.GetGetMethod()!;
            var target = Expression.Parameter(typeof(object));
            var convert = Expression.Convert(target, asyncLocal.GetType());
            var getValue = Expression.Call(convert, method);

            return Expression.Lambda<Func<object, object>>(getValue, target).Compile();
        }

        static Func<object, HttpContext> CreateHttpContextAccessor(object holder)
        {
            var target = Expression.Parameter(typeof(object));
            var convert = Expression.Convert(target, holder.GetType());
            var field = Expression.Field(convert, "Context");
            var convertAsResult = Expression.Convert(field, typeof(HttpContext));

            return Expression.Lambda<Func<object, HttpContext>>(convertAsResult, target).Compile();
        }
    }

    public static string Encrypt(string password)
    {
        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        byte[] salt;
        byte[] subkey;

        using var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, PbKDF2IterCount);

        salt = deriveBytes.Salt;
        subkey = deriveBytes.GetBytes(PbKDF2SubkeyLength);

        var outputBytes = new byte[1 + SaltSize + PbKDF2SubkeyLength];
        Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
        Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PbKDF2SubkeyLength);

        return Convert.ToBase64String(outputBytes);
    }

    public static bool Validate(string hashed, string origin)
    {
        if (hashed == null)
        {
            return false;
        }

        if (origin == null)
        {
            throw new ArgumentNullException(nameof(origin));
        }

        var hashedPasswordBytes = Convert.FromBase64String(hashed);

        if (hashedPasswordBytes.Length != 1 + SaltSize + PbKDF2SubkeyLength || hashedPasswordBytes[0] != 0x00)
        {
            return false;
        }

        var salt = new byte[SaltSize];
        Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
        var storedSubkey = new byte[PbKDF2SubkeyLength];
        Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PbKDF2SubkeyLength);

        byte[] generatedSubkey;
        using var deriveBytes = new Rfc2898DeriveBytes(origin, salt, PbKDF2IterCount);

        generatedSubkey = deriveBytes.GetBytes(PbKDF2SubkeyLength);

        return ByteArraysEqual(storedSubkey, generatedSubkey);
    }

    [MethodImpl(MethodImplOptions.NoOptimization)]
    private static bool ByteArraysEqual(byte[] a, byte[] b)
    {
        if (ReferenceEquals(a, b))
        {
            return true;
        }

        if (a == null || b == null || a.Length != b.Length)
        {
            return false;
        }

        var areSame = true;
        for (var i = 0; i < a.Length; i++)
        {
            areSame &= a[i] == b[i];
        }

        return areSame;
    }
}